feat: add Windows title bar theme support#136
Conversation
📝 WalkthroughWalkthroughAdds a Windows-only title-bar theming module and Tauri command ChangesTitle Bar Theming (frontend + backend)
Sequence DiagramsequenceDiagram
participant React as React Theme Context
participant Tauri as Tauri Command Handler
participant WinAPI as Windows API (DwmSetWindowAttribute)
participant Window as Native Window Title Bar
React->>Tauri: invoke("set_title_bar_theme", {isDark, r, g, b})
activate Tauri
Tauri->>Tauri: enumerate webview_windows() (labels "main"/"preview-*")
Tauri->>WinAPI: apply_title_bar_theme(window, isDark, (r,g,b))
activate WinAPI
WinAPI->>Window: set immersive dark + caption/border colors
deactivate WinAPI
Tauri-->>React: Ok(())
deactivate Tauri
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src-tauri/src/lib.rs (2)
3892-3903: Preview windows won't receive title bar theme updates.The command only applies the theme to the
"main"window. Preview windows (labeled"preview-*") created viacreate_preview_windowwon't have their title bars themed, causing visual inconsistency when editing files outside the notes folder.Consider iterating over all windows or accepting an optional window label:
🔧 Suggested fix to theme all windows
#[tauri::command] fn set_title_bar_theme(app: AppHandle, is_dark: bool) -> Result<(), String> { #[cfg(target_os = "windows")] { - if let Some(window) = app.get_webview_window("main") { - windows_title_bar::apply_title_bar_theme(&window, is_dark); + for (_, window) in app.webview_windows() { + windows_title_bar::apply_title_bar_theme(&window, is_dark); } } let _ = app; let _ = is_dark; Ok(()) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src-tauri/src/lib.rs` around lines 3892 - 3903, The set_title_bar_theme command currently only themes the "main" window; update it to apply the theme to all relevant windows (e.g., the "main" window and any preview windows created by create_preview_window with labels starting with "preview-") by iterating over app.windows() (or app.hooks/windows API) and calling windows_title_bar::apply_title_bar_theme(&window, is_dark) for each matching window label, or alternatively extend set_title_bar_theme to accept an optional window_label parameter and apply the theme only to that label if provided; adjust references to set_title_bar_theme and create_preview_window accordingly so preview windows receive updates too.
3705-3708: Initial title bar theme doesn't respect system preference.At startup,
apply_title_bar_themeis called withis_dark: false, regardless of the actual system theme. This may cause a brief flash of light title bar before the frontend'sThemeContextinvokesset_title_bar_themewith the correct resolved theme.Consider reading the system theme preference here, or deferring this call until after the frontend initializes:
🔧 Suggested fix to respect system preference at startup
#[cfg(target_os = "windows")] { - windows_title_bar::apply_title_bar_theme(&main_window, false); + // Let the frontend apply the theme after resolving user/system preference + // to avoid a brief flash of incorrect title bar color }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src-tauri/src/lib.rs` around lines 3705 - 3708, The Windows title bar is forced to light by calling windows_title_bar::apply_title_bar_theme(&main_window, false) at startup; change this to respect system preference by querying the system theme and passing the actual dark/light boolean (or defer the call and let the frontend call set_title_bar_theme after ThemeContext resolves). Update the #[cfg(target_os = "windows")] block: replace the hardcoded false with the system theme check (or remove the call and rely on set_title_bar_theme) so apply_title_bar_theme and main_window use the correct initial is_dark value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Around line 3892-3903: The set_title_bar_theme command currently only themes
the "main" window; update it to apply the theme to all relevant windows (e.g.,
the "main" window and any preview windows created by create_preview_window with
labels starting with "preview-") by iterating over app.windows() (or
app.hooks/windows API) and calling
windows_title_bar::apply_title_bar_theme(&window, is_dark) for each matching
window label, or alternatively extend set_title_bar_theme to accept an optional
window_label parameter and apply the theme only to that label if provided;
adjust references to set_title_bar_theme and create_preview_window accordingly
so preview windows receive updates too.
- Around line 3705-3708: The Windows title bar is forced to light by calling
windows_title_bar::apply_title_bar_theme(&main_window, false) at startup; change
this to respect system preference by querying the system theme and passing the
actual dark/light boolean (or defer the call and let the frontend call
set_title_bar_theme after ThemeContext resolves). Update the #[cfg(target_os =
"windows")] block: replace the hardcoded false with the system theme check (or
remove the call and rely on set_title_bar_theme) so apply_title_bar_theme and
main_window use the correct initial is_dark value.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d1fc1545-3c61-42a6-94ea-fc65d97dbff4
📒 Files selected for processing (2)
src-tauri/src/lib.rssrc/context/ThemeContext.tsx
…order The hardcoded caption color literals were encoded as 0x00RRGGBB but DwmSetWindowAttribute expects COLORREF (0x00BBGGRR), so the title bar was a few bits off from --color-bg-secondary. With erictli#134 letting users customize theme colors, the gap also widened beyond the defaults. Pass the resolved bg-secondary color from the frontend as an RGB triple and build the COLORREF correctly in Rust. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/context/ThemeContext.tsx (2)
482-489: 💤 Low valueConsider logging the error instead of silently swallowing it.
Other backend invocations in this file (
saveThemeSettings,saveFontSettings,setTextDirection,setEditorWidth,setCustomColor, etc.) all log failures viaconsole.error. Swallowing errors here makes future debugging of title-bar regressions on Windows harder, with no real benefit since this is dev/console-only output.♻️ Suggested change
if (rgb) { invoke("set_title_bar_theme", { isDark: resolvedTheme === "dark", r: rgb[0], g: rgb[1], b: rgb[2], - }).catch(() => {}); + }).catch((err) => + console.error("Failed to sync title bar theme:", err), + ); }As per coding guidelines: Implement error handling with user-friendly messages.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/context/ThemeContext.tsx` around lines 482 - 489, The catch on the invoke("set_title_bar_theme", ...) call is swallowing errors; update the catch to log the error (e.g., console.error) including context such as the function call and values like resolvedTheme and rgb so failures in set_title_bar_theme are visible during debugging; locate the block in ThemeContext.tsx where invoke("set_title_bar_theme", { isDark: resolvedTheme === "dark", r: rgb[0], g: rgb[1], b: rgb[2] }) is called and replace the empty .catch(() => {}) with a .catch(err => console.error("set_title_bar_theme failed", { err, resolvedTheme, rgb })) or equivalent.
79-92: 💤 Low valueOptional: avoid DOM mutation by using a canvas to normalize colors.
parseCssColorToRgbworks for the current input set (hex +rgb()/rgba()), but it appends/removes a<div>and triggers a style recompute on every theme/color change. A canvas-based normalizer is allocation-free and side-effect-free, and yields the samergb(r, g, b[, a])form back fromfillStyle.Also note the regex only handles integer comma-separated channels —
rgb(0 0 0)(CSS Color 4 space-separated) andcolor(display-p3 …)(returned by some Chromium versions for wide-gamut inputs) would silently drop the title-bar update. Probably fine given current inputs, just worth being aware of.♻️ Canvas-based alternative (no DOM mutation)
-function parseCssColorToRgb(value: string): [number, number, number] | null { - if (typeof document === "undefined") return null; - const probe = document.createElement("div"); - probe.style.color = value; - probe.style.display = "none"; - document.body.appendChild(probe); - const computed = getComputedStyle(probe).color; - document.body.removeChild(probe); - const match = computed.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); - if (!match) return null; - return [Number(match[1]), Number(match[2]), Number(match[3])]; -} +function parseCssColorToRgb(value: string): [number, number, number] | null { + if (typeof document === "undefined") return null; + const ctx = document.createElement("canvas").getContext("2d"); + if (!ctx) return null; + ctx.fillStyle = "#000"; // reset to a known value first + ctx.fillStyle = value; // canvas rejects invalid colors, leaving the previous value + const normalized = ctx.fillStyle as string; + const match = normalized.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/); + if (!match) return null; + return [Number(match[1]), Number(match[2]), Number(match[3])]; +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/context/ThemeContext.tsx` around lines 79 - 92, parseCssColorToRgb currently mutates the DOM by creating/removing a probe DIV and uses a limited regex; replace its body with a canvas-based parser: if document/canvas is unavailable return null, create an off-screen HTMLCanvasElement (or reuse a cached one) and 2D context, set ctx.fillStyle = value, read back ctx.fillStyle (which normalizes to an rgb(a) string), then parse that string with a more robust regex that accepts comma- or space-separated channels and decimal values (capture r,g,b as numbers, ignore alpha if present) and return [r,g,b] or null on failure; reference the function name parseCssColorToRgb and ensure no DOM mutations occur and that SSR environments safely return null.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/context/ThemeContext.tsx`:
- Around line 482-489: The catch on the invoke("set_title_bar_theme", ...) call
is swallowing errors; update the catch to log the error (e.g., console.error)
including context such as the function call and values like resolvedTheme and
rgb so failures in set_title_bar_theme are visible during debugging; locate the
block in ThemeContext.tsx where invoke("set_title_bar_theme", { isDark:
resolvedTheme === "dark", r: rgb[0], g: rgb[1], b: rgb[2] }) is called and
replace the empty .catch(() => {}) with a .catch(err =>
console.error("set_title_bar_theme failed", { err, resolvedTheme, rgb })) or
equivalent.
- Around line 79-92: parseCssColorToRgb currently mutates the DOM by
creating/removing a probe DIV and uses a limited regex; replace its body with a
canvas-based parser: if document/canvas is unavailable return null, create an
off-screen HTMLCanvasElement (or reuse a cached one) and 2D context, set
ctx.fillStyle = value, read back ctx.fillStyle (which normalizes to an rgb(a)
string), then parse that string with a more robust regex that accepts comma- or
space-separated channels and decimal values (capture r,g,b as numbers, ignore
alpha if present) and return [r,g,b] or null on failure; reference the function
name parseCssColorToRgb and ensure no DOM mutations occur and that SSR
environments safely return null.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 93a77b3a-aa96-43c8-b55a-df5ac6a29895
📒 Files selected for processing (2)
src-tauri/src/lib.rssrc/context/ThemeContext.tsx
erictli
left a comment
There was a problem hiding this comment.
Thanks for the contribution @chengcheng84 - I made it work with custom theme colors.
Before
2026-04-06.093820.mp4
Now
2026-04-06.093437.mp4
Summary by CodeRabbit